Apple Events in PP
Volume Number: 11
Issue Number: 6
Column Tag: Applying Apple Technology
Powering Up AppleEvents in PowerPlant
Add an advanced feature to this popular framework
By Jeremy Roschelle
Note: Source code files accompanying article are located on MacTech CD-ROM or
source code disks.
One of System 7’s most advanced features is its support for system-level scripting via
AppleEvents. The AppleScript programming language can give users the ability to
automate tasks, customize the interface, and connect independent applications to
perform a task. Although Apple has presented these capabilities as advantages to
business users, they are an powerful addition to educational software - what I create.
In my present project, “SimCalc,” we decided to add scripting to give students,
teachers and curriculum authors the ability to set up mathematical simulations.
Unfortunately, AppleEvents sports System 7’s least comprehensible
documentation. Although AppleEvents sounded attractive, dense writing in Inside Mac
VI sent me running for cover. With the arrival of better documentation and direct
support for the AppleEvents Object Model (AEOM) in the PowerPlant class library, I
decided to come out from hiding. After learning many lessons the hard way, SimCalc is
now fully scriptable.
This article brings together many bits of necessary information that I discovered
along the way. It will guide you through the process of using PowerPlant to make your
application “scriptable” - able to respond to AppleEvent commands written in
AppleScript or other scripting systems. However, no attempt is made to fully explain
the AppleEvent Object Model or the concept of a “recordable” application. For that
information, see Inside Mac: InterApplication Communication. AppleEvents and
AppleScript are powerful capabilities, and by inheriting the basics from PowerPlant
classes, you can easily give your users the power to write scripts that manipulate
your application’s objects.
Pieces of the Puzzle
One of the difficulties in getting started with AppleEvents is making several
components work smoothly with each other. Here is an overview:
The AppleEvent Object Model (AEOM) defines a generic view of a scriptable
application. Every application presents itself to AppleScript as a containment
hierarchy. For example, in our SimCalc application, the application contains
documents, documents contain pages, and pages contain graphs. Each object in the
hierarchy has properties that the user can get and set. Events operate over the objects
in the hierarchy, creating and deleting them, and changing their properties.
An application’s “terminology resource” (resource type ‘aete’)
presents the application’s AEOM interface to other applications and scripts. It
describes your application’s containment hierarchy, along with the actions possible on
objects in the hierarchy. The ‘aete’ resource also translates between the English-like
AppleScript vocabulary and four-character descriptors that your application can
easily respond to. There are descriptors for classes, properties, and event names and
parameters.
PowerPlant provides several support classes to handle AppleEvent interaction.
The central class is LModelObject, which handles properties, events, and containment
relations for one class in the containment hierarchy. By deriving your classes from
LModelObject, handling AppleEvents becomes a relatively simple matter of
overriding appropriate methods.
ScriptEditor, which is part of the AppleScript kit and included with System
7.5, is helpful for testing your work. It provides tools for reading an applications
‘aete’ terminology dictionary, writing and checking the syntax of scripts against the
terminology resource, and compiling and executing the script.
AEOM Conventions and Terms
Awareness of a few conventions and terms will make reading this article easier. The
AppleEvent Object Model uses the standard separation of an application into verbs and
nouns. Each verb is an “Event” and each noun is a “Class.” For example, the
statement “Close Window 1” consists of the “close” event applied to the first object in
the “window” class. Events have a “direct object,” which, by convention, is the noun
to which the verb is applied (e.g., the window). Events take a list of
keyword-specified parameters and can return a value.
A class corresponds to one level in the containment hierarchy. In our
application, we have a class for the application, document, page, and graph level,
among many others. An AEOM class need not correspond to a C++ implementation
class; in particular, you don’t need an AEOM class for every C++ class, just those that
will be direct objects of the Events you implement. A class has “Properties” to
represent its state. For example, for example a page includes a name and index
number as properties. The containment hierarchy is organized by the “Elements”
relationship: in our application, the elements of the application class are documents,
the elements of the document class are pages, and the elements of the page class are
graphs.
PowerPlant uses the “L” prefix to distinguish library classes and we use “SC”
to distinguish SimCalc classes.
Organization of the Article
This article divides the task of making your application scriptable into five parts:
1. adding a new class to the model hierarchy
2. adding a property to the class
3. adding a generic undo facility that supports every property.
4. adding an event handler for a standard event
5. adding a custom event
Each of these processes will require changes to the C++ classes derived from the
PowerPlant class library, as well as changes to the terminology resource. Each change
will be tested using ScriptEditor. Organizing the effort by processes makes it easier to
see how AEOM, the terminology resource, PowerPlant, and Script Editor interrelate.
Adding a New Class
In PowerPlant, LApplication, LDocument, and LWindow inherit from LModelObject,
which provides the root of the containment hierarchy. The terminology resource that
comes with PowerPlant already specifies the “Elements” relationship among these
classes, as well as the properties of each class.
In our SimCalc project, our documents are notebooks consisting of a list of pages.
We want to support our notion of a page in the AEOM containment hierarchy. This
requires changes in both the terminology resource and the SimCalc C++ classes.
Specifically, we add the Page class to the terminology resource in two places, define
appropriate methods for our SCPage class, and override some LModelObject methods
in the containing class, SCDoc.
A good place to start making your application scriptable is with the terminology
resource. Resorcerer provides a very good editor for the ‘aete’ resource. We start
with the ‘aete’ supplied with PowerPlant, which describes the application, windows,
and documents. To this we add information about the existence of the page class, and
use the Elements relationship to indicate that documents contain pages.
Information in the terminology resource is organized into suites, with standard
suites defined in the AppleEvents Registry, and custom suites defined elsewhere. Since
pages are specific to our application, we create a new suite called the SimCalc suite.
The name of the suite and its description are arbitrary, but your users will see them
when they read your ‘aete’ resource with ScriptEditor’s “Open Dictionary” command.
The suite id should be unique (registering it with Apple will maintain uniqueness).
Figure 1: Adding the Page Class to the SimCalc Suite
Adding class to the containment hierarchy involves two operations - creating the
class and specifying which container has elements of this type. We’ll add our class to
the SimCalc suite. To do this in Resorcerer, position the cursor within the classes box
and press the New button. The class name you provide will be used by AppleScript to
name the class in English, so choose a short noun. I find it helpful to capitalize the
first letter of all English names for debugging: ScriptEditor’s syntax editor will
replace statements with the capitalization you provide. Therefore if you type
statements into ScriptEditor in lowercase, you can tell if ScriptEditor is recognizing
your names by the capitalization.
The class id you choose will be used to identify objects of this class inside your
application, and therefore must be unique within your ‘aete’. The description of the
class is not functionally important, but will be read by users as they try to understand
what the class represents.
In our application documents contain pages. In the terminology resource, the
“Elements” relationship is indicated in the containing class. To specify that pages are
elements of documents, we edit the definition of the document class, adding the class id
of the page class to its elements field. In addition to adding the element, you must
specify how the element can be accessed from the container. The most common ways to
access a document are by name and index, and our pages also have unique ids, so we
added that to the list.
Incidentally, Resorcerer doesn’t know about the ‘ID ’ form code; you have to type
it in by hand. Also “absolute position” is a bit misleading, since PowerPlant can use
this information to compute a variety of relative references as well. References to the
first, last, middle element, as well as reference to an object before or after another
object are computed from an absolute position reference.
Figure 2: Specifying that a page is an element in the document
Now we have completed editing the containment hierarchy. To check our work,
we can open the terminology resource from ScriptEditor’s Open Dictionary command.
Figure 3: ScriptEditor shows that pages have been added to documents
ScriptEditor parses the information provided, and presents it in readable form.
Because we created a class with the name “Page” and made it an element of documents,
ScriptEditor can check the syntax of scripts that refer to pages with a variety of
reference forms:
tell application "PoweringUp
get Page 1 -- by absolute position
get last Page -- uses absolute position
get Page "first" -- by name
get Page after Page "first" -- by relative position
get Page id 10 -- by id
end tell
Now its time to make the application recognize these references. Fortunately,
PowerPlant makes this fairly easy. Each object only needs to know about its
“SuperModel” (i.e., the container in which it resides) and any submodels it contains.
The SuperModel-SubModel relationship is the same as the “Elements” containment
relationship. The containment relationship is different from the Class-SubClass
inheritance relationship; AEOM has no good way to express inheritance relationships .
In the SimCalc example, a page needs to know its containing document (its
SuperModel), and the document needs to be able to locate pages as SubModels. In
addition, the class must override GetModelKind, which is used by PowerPlant to
generate an external description of an object. Returning ‘page’ will let PowerPlant
know that this particular object is a page.
The external description should be the same as the class id defined in the
terminology resource. In our case, the class definition looks like this:
SCPage.h
class SCPage : public LModelObject {
public:
enum { modelKind = 'page'} ;
SCPage(SCDoc *inDoc);
virtual ~SCPage();
virtual DescType GetModelKind() const;
};
The constructor sets up the object’s SuperModel. The SuperModel is the object
that contains this class in the containment hierarchy.
SCPage::SCPage
SCPage:SCPage(SCDoc *inDoc) : LModelObject()
{
mSuperModel = inDoc;
inDoc->AddSubModel(this); // stores the page in the Doc's list
}
SCPage::~SCPage
SCPage:~SCPage(SCDoc *inDoc) : LModelObject()
{
mSuperModel->RemoveSubModel(this)
mSuperModel = nil; // prevents removal by LModelObject
}
The GetModelKind member function overrides the default, and returns the class
id.
SCPage::GetModelKind
DescType
SCPage::GetModelKind() const
{
return SCPage::modelKind;
}
In the SimCalc document class we store each page in a mPageList member
variable, which is an LList. To maintain this list, we override the
LModelObject::AddSubModel method, which gets called by the SCPage constructor.
Now we have ensured that pages refer to the document as its SuperModel, and the
document refers to pages as its elements.
SCDoc::AddSubModel
void
SCDoc::AddSubModel(LModelObject *inSubModel)
{
if (SCPage::modelKind == inSubModel->GetModelKind()) {
mPageList.InsertItemsAt(1,arrayIndex_Last,&inSubModel);
}
}
SCDoc::RemoveSubModel
void
SCDoc::RemoveSubModel(LModelObject *inSubModel)
{
if (SCPage::modelKind == inSubModel->GetModelKind()) {
mPageList.Remove(inSubModel);
}
}
While these methods are simple, a subtle C++ gotcha is carefully avoided. In the
SCPage constructor, we could have set up the SuperModel-SubModel relationship by
passing inDoc in the LModelObject initializer, i.e.,
SCPage:SCPage(SCDoc *inDoc) : LModelObject(inDoc)
{
}
If we did this, the dynamic type check in SCDoc::AddSubModel would return the
wrong value. It turns out that GetModelKind would return typeNull instead of
scPage. Why?
AddSubModel is called within the LModelObject constructor, and when this
constructor is called, the SCPage vtable is not yet constructed, and the object is just
an LModelObject. When the SCEleDoc method asks for its type,
LModelObject::GetModelKind is called, instead of SCPage::GetModelKind. Gotcha!
The same problem occurs upon deletion. When the LModelObject destructor is
called, the vtable has already been deconstructed from an SCPage to a LModelObject,
so GetModelKind returns typeNull again. (So in our destructor, we manually break
the super-sub model relationship instead.)